' Stephen Toub
' Coded and published in January 2007 issue of MSDN Magazine 
' http://msdn.microsoft.com/msdnmag/issues/07/01/PreviewHandlers/default.aspx


Imports Microsoft.VisualBasic
Imports System
Imports System.IO
Imports System.Text
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Imports System.Collections.Generic
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.ConstrainedExecution
Imports System.Security
Imports System.Security.Permissions
Imports System.ComponentModel
Imports C4F.DevKit.PreviewHandler.PreviewHandlerFramework

Namespace C4F.DevKit.PreviewHandler.PreviewHandlers.PreviewHandlers
    <PreviewHandler("MSDN Magazine MSI Preview Handler", ".msi", "{63cf7c29-dd45-4906-ac42-bb117d816f65}"), ProgId("C4F.DevKit.PreviewHandler.PreviewHandlers.MsiPreviewHandler"), Guid("21c532d1-6f1a-4f53-a000-0468a4337ab5"), ClassInterface(ClassInterfaceType.None), ComVisible(True)> _
    Public NotInheritable Class MsiPreviewHandler
        Inherits FileBasedPreviewHandler
        Protected Overrides Function CreatePreviewHandlerControl() As PreviewHandlerControl
            Return New MsiPreviewHandlerControl()
        End Function

        Private NotInheritable Class MsiPreviewHandlerControl
            Inherits FileBasedPreviewHandlerControl
            Public Overrides Sub Load(ByVal file As FileInfo)
                Dim listView As ListView = New ListView()

                listView.Columns.Add("File Name", -2)
                listView.Columns.Add("Size", -2)
                listView.Columns.Add("Version", -2)

                listView.Dock = DockStyle.Fill
                listView.BorderStyle = BorderStyle.None

                listView.FullRowSelect = True
                listView.HeaderStyle = ColumnHeaderStyle.Nonclickable
                listView.MultiSelect = False
                listView.SmallImageList = _iconProvider.Icons
                listView.LargeImageList = listView.SmallImageList
                listView.View = View.Details

                For Each msiFile As MsiFileInfo In GetFiles(file.FullName)
                    listView.Items.Add(New ListViewItem(New String() {msiFile.FileName, msiFile.Size, msiFile.Version}, _iconProvider.GetIconIndexForFile(msiFile.FileName)))
                Next msiFile

                Controls.Add(listView)
            End Sub

            Private Shared Function GetFiles(ByVal msiPath As String) As List(Of MsiFileInfo)
                Dim msiFiles As List(Of MsiFileInfo) = New List(Of MsiFileInfo)()

                ' Open database
                Dim dbHandleVal As Integer
                If MsiOpenDatabase(msiPath, IntPtr.Zero, dbHandleVal) <> 0 Then
                    Throw New Win32Exception()
                End If
                Using dbHandle As MsiHandle = New MsiHandle(dbHandleVal)
                    ' Make sure file table exists
                    Dim tableName As String = "File"
                    If MsiDatabaseIsTablePersistent(dbHandle.Handle, tableName) = 1 Then
                        ' Opoen the view of the list of files
                        Dim query As String = "SELECT * FROM `" & tableName & "`"
                        Dim viewHandleVal As Integer
                        If MsiDatabaseOpenView(dbHandle.Handle, query, viewHandleVal) <> 0 Then
                            Throw New Win32Exception()
                        End If
                        Using viewHandle As MsiHandle = New MsiHandle(viewHandleVal)
                            If MsiViewExecute(viewHandle.Handle, 0) <> 0 Then
                                Throw New Win32Exception()
                            End If

                            ' Get a list of the columns in order to find the name field
                            Dim fieldToIndexMapping As Dictionary(Of String, Integer) = New Dictionary(Of String, Integer)()
                            Dim columnNamesRecordVal As Integer
                            If MsiViewGetColumnInfo(viewHandle.Handle, 0, columnNamesRecordVal) <> 0 Then
                                Throw New Win32Exception()
                            End If
                            Using columnNamesRecord As MsiHandle = New MsiHandle(columnNamesRecordVal)
                                Dim fieldCount As Integer = MsiRecordGetFieldCount(columnNamesRecord.Handle)
                                For i As Integer = 0 To fieldCount - 1
                                    fieldToIndexMapping(GetRecordString(columnNamesRecord, i)) = i
                                Next i
                            End Using
                            If fieldToIndexMapping.ContainsKey("FileName") Then
                                ' Get the files list
                                Dim rv As UInteger
                                Dim recordVal As Integer
                                Do While (rv = MsiViewFetch(viewHandle.Handle, recordVal)) = 0
                                    Using record As MsiHandle = New MsiHandle(recordVal)
                                        Dim fileNameParts As String() = GetRecordString(record, fieldToIndexMapping("FileName")).Split(New Char() {"|"c}, StringSplitOptions.RemoveEmptyEntries)
                                        Dim fileName As String
                                        If fileNameParts.Length > 1 Then
                                            fileName = fileNameParts(1)
                                        Else
                                            fileName = fileNameParts(0)
                                        End If
                                        Dim size As String = GetRecordString(record, fieldToIndexMapping("FileSize"))
                                        Dim version As String = GetRecordString(record, fieldToIndexMapping("Version"))
                                        msiFiles.Add(New MsiFileInfo(fileName, version, size))
                                    End Using
                                Loop
                                If rv <> &H103 Then
                                    Throw New Win32Exception()
                                End If
                            End If
                        End Using
                    End If
                End Using
                Return msiFiles
            End Function

            Private Structure MsiFileInfo
                Public Sub New(ByVal fileName As String, ByVal version As String, ByVal size As String)
                    Me.FileName = fileName
                    Me.Version = version
                    Me.Size = size
                End Sub
                Public ReadOnly FileName As String
                Public ReadOnly Version As String
                Public ReadOnly Size As String
            End Structure

            Private Shared Function GetRecordString(ByVal record As MsiHandle, ByVal field As Integer) As String
                Dim length As Integer = 255
                Dim rv As UInteger = 0
                Dim text As StringBuilder = New StringBuilder(length)
                If (rv = MsiRecordGetString(record.Handle, field, text, length)) = &HEA Then
                    length += 1
                    text.EnsureCapacity(length)
                    rv = MsiRecordGetString(record.Handle, field, text, length)
                End If
                If rv <> 0 Then
                    Throw New Win32Exception()
                End If
                Return text.ToString()
            End Function

            <DllImport("msi.dll", EntryPoint:="MsiOpenDatabaseW", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiOpenDatabase(ByVal databasePath As String, ByVal persist As IntPtr, <System.Runtime.InteropServices.Out()> ByRef database As Integer) As UInteger
            End Function

            <DllImport("msi.dll", EntryPoint:="MsiDatabaseIsTablePersistentW", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiDatabaseIsTablePersistent(ByVal database As Integer, ByVal tableName As String) As Integer
            End Function

            <DllImport("msi.dll", EntryPoint:="MsiDatabaseOpenViewW", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiDatabaseOpenView(ByVal database As Integer, ByVal query As String, <System.Runtime.InteropServices.Out()> ByRef view As Integer) As UInteger
            End Function

            <DllImport("msi.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiViewExecute(ByVal view As Integer, ByVal record As Integer) As UInteger
            End Function

            <DllImport("msi.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiViewFetch(ByVal view As Integer, <System.Runtime.InteropServices.Out()> ByRef record As Integer) As UInteger
            End Function

            <DllImport("msi.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiViewGetColumnInfo(ByVal view As Integer, ByVal columnInfo As Integer, <System.Runtime.InteropServices.Out()> ByRef record As Integer) As UInteger
            End Function

            <DllImport("msi.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiRecordGetFieldCount(ByVal record As Integer) As Integer
            End Function

            <DllImport("msi.dll", EntryPoint:="MsiRecordGetStringW", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
            Private Shared Function MsiRecordGetString(ByVal record As Integer, ByVal field As Integer, ByVal valueBuf As StringBuilder, ByRef valueBufSize As Integer) As UInteger
            End Function

            ' An MSI handle is a 32-bit value, even on 64-bit platforms.  As such,
            ' an actual SafeHandle-derived type is inappropriate.
            Private NotInheritable Class MsiHandle
                Inherits CriticalFinalizerObject
                Implements IDisposable
                Private _handle As Integer

                Public Sub New(ByVal handle As Integer)
                    _handle = handle
                End Sub

                Public ReadOnly Property Handle() As Integer
                    Get
                        Return _handle
                    End Get
                End Property

                <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), DllImport("msi.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> _
                Private Shared Function MsiCloseHandle(ByVal database As Integer) As UInteger
                End Function

                Private Sub IDisposable_Dispose() Implements IDisposable.Dispose
                    GC.SuppressFinalize(Me)
                    Dispose()
                End Sub

                <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)> _
                Private Sub Dispose()
                    If _handle <> 0 Then
                        MsiCloseHandle(_handle)
                    End If
                    _handle = 0
                End Sub

                Protected Overrides Sub Finalize()
                    Dispose()
                End Sub
            End Class

            Private _iconProvider As FileTypeIconProvider = New FileTypeIconProvider()

            Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
                If disposing AndAlso Not _iconProvider Is Nothing Then
                    _iconProvider.Dispose()
                    _iconProvider = Nothing
                End If
                MyBase.Dispose(disposing)
            End Sub
        End Class
    End Class
End Namespace